Skip to content

Conversation

@gesslerpd
Copy link
Contributor

@gesslerpd gesslerpd commented Jan 6, 2026

Previously, negative timestamps (representing dates before 1970-01-01) were not supported on Windows due to platform limitations. The changes introduce a fallback implementation using the Windows FILETIME API, allowing negative timestamps to be correctly handled in both UTC and local time conversions. Additionally, related test code is updated to remove Windows-specific skips and error handling, ensuring consistent behavior across platforms.

This may have some overlap and/or impact on the proposed changes in #134461

Other related issues:

@gesslerpd gesslerpd changed the title gh-80620: Support negative timestamps on windows for datetime.datetime gh-80620: Support negative timestamps on windows in datetime module Jan 6, 2026
@gesslerpd gesslerpd force-pushed the windows-datetime-pre-epoch branch from 0b9b5e8 to 64c70c3 Compare January 6, 2026 04:24
Copy link
Member

@vstinner vstinner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to change datetime in a separated PR, and restrict this change to time.gmtime() and time.localtime() changes.

You should document time.gmtime() and time.localtime() changes in a NEWS entry.

Python/pytime.c Outdated
Comment on lines 328 to 344
/* Calculate day of year using Windows FILETIME difference */
// SYSTEMTIME st_jan1 = {st_result.wYear, 1, 0, 1, 0, 0, 0, 0};
// FILETIME ft_jan1, ft_date;
// if (!SystemTimeToFileTime(&st_jan1, &ft_jan1) ||
// !SystemTimeToFileTime(&st_result, &ft_date)) {
// PyErr_SetFromWindowsErr(0);
// return -1;
// }
// ULARGE_INTEGER jan1, date;
// jan1.LowPart = ft_jan1.dwLowDateTime;
// jan1.HighPart = ft_jan1.dwHighDateTime;
// date.LowPart = ft_date.dwLowDateTime;
// date.HighPart = ft_date.dwHighDateTime;
// /* Convert 100-nanosecond intervals to days */
// LONGLONG days_diff = (date.QuadPart - jan1.QuadPart) / (24LL * 60 * 60 * HUNDRED_NS_PER_SEC);

// tm->tm_yday = (int)days_diff;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove commented/dead code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think that yday is important to have? Doesn't impact datetime AFAIK but it does show up in struct_time objects returned from gmtime/localtime. For completeness with respect to time module it could be put back in.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If possible, it would be better to fill tm_yday as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Restored tm_yday calculation, I think the windows API version was a bit heavy so simplified it by adding custom util function.

@vstinner
Copy link
Member

vstinner commented Jan 8, 2026

test_time.test_mktime() can be updated: except (OverflowError, OSError) is no longer needed if I'm right.

@gesslerpd
Copy link
Contributor Author

test_time.test_mktime() can be updated: except (OverflowError, OSError) is no longer needed if I'm right.

time.mktime still doesn't support negative timestamps with this change, gmtime and localtime do as side effect, was mostly making these changes for datetime.

I was going to tweak callsites so it doesn't impact time module at all but thought I'd leave that choice to the review.

@vstinner
Copy link
Member

vstinner commented Jan 8, 2026

I was going to tweak callsites so it doesn't impact time module at all but thought I'd leave that choice to the review.

Oh, it's a good thing to implement the new feature in the time module.


Existing test_time test:

    def test_mktime(self):
        # Issue #1726687
        for t in (-2, -1, 0, 1):
            try:
                tt = time.localtime(t)
            except (OverflowError, OSError):
                pass
            else:
                self.assertEqual(time.mktime(tt), t)

If localtime() is modified to support negative timestamp, but mktime() can still fail, the test should be modified to something like:

        for t in (-2, -1, 0, 1):
            tt = time.localtime(t)
            try:
                t2 = time.mktime(tt)
            except (OverflowError, OSError):
                pass
            else:
                self.assertEqual(t2, t)

@gesslerpd gesslerpd changed the title gh-80620: Support negative timestamps on windows in datetime module gh-80620: Support negative timestamps on windows in time.gmtime, time.localtime, and datetime module Jan 8, 2026
Copy link
Member

@vstinner vstinner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for additional test on leap and non-leap years.

Copy link
Member

@vstinner vstinner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. I just have minor coding style suggestions on comments.

@gesslerpd
Copy link
Contributor Author

Suggestions applied, thanks again for review!

@vstinner vstinner merged commit f5685a2 into python:main Jan 15, 2026
47 checks passed
@vstinner
Copy link
Member

Merged, thanks. This was a long awaited feature :-)

@bedevere-bot
Copy link

⚠️⚠️⚠️ Buildbot failure ⚠️⚠️⚠️

Hi! The buildbot x86 Debian Installed with X 3.x (no tier) has failed when building commit f5685a2.

What do you need to do:

  1. Don't panic.
  2. Check the buildbot page in the devguide if you don't know what the buildbots are or how they work.
  3. Go to the page of the buildbot that failed (https://buildbot.python.org/#/builders/1244/builds/7242) and take a look at the build logs.
  4. Check if the failure is related to this commit (f5685a2) or if it is a false positive.
  5. If the failure is related to this commit, please, reflect that on the issue and make a new Pull Request with a fix.

You can take a look at the buildbot page here:

https://buildbot.python.org/#/builders/1244/builds/7242

Summary of the results of the build (if available):

==

Click to see traceback logs
Traceback (most recent call last):
  File �[35m"/buildbot/buildarea/3.x.ware-debian-x86.installed/build/target/lib/python3.15/test/test_time.py"�[0m, line �[35m208�[0m, in �[35mtest_gmtime�[0m
    res = time.gmtime(t)
�[1;35mOverflowError�[0m: �[35mtimestamp out of range for platform time_t�[0m

@bedevere-bot
Copy link

⚠️⚠️⚠️ Buildbot failure ⚠️⚠️⚠️

Hi! The buildbot ARM Raspbian 3.x (tier-3) has failed when building commit f5685a2.

What do you need to do:

  1. Don't panic.
  2. Check the buildbot page in the devguide if you don't know what the buildbots are or how they work.
  3. Go to the page of the buildbot that failed (https://buildbot.python.org/#/builders/424/builds/12631) and take a look at the build logs.
  4. Check if the failure is related to this commit (f5685a2) or if it is a false positive.
  5. If the failure is related to this commit, please, reflect that on the issue and make a new Pull Request with a fix.

You can take a look at the buildbot page here:

https://buildbot.python.org/#/builders/424/builds/12631

Failed tests:

  • test_time

Failed subtests:

  • test_gmtime - test.test_time.TimeTestCase.test_gmtime

Summary of the results of the build (if available):

==

Click to see traceback logs
Traceback (most recent call last):
  File "/var/lib/buildbot/workers/3.x.gps-raspbian.nondebug/build/Lib/test/test_time.py", line 208, in test_gmtime
    res = time.gmtime(t)
OverflowError: timestamp out of range for platform time_t

@vstinner
Copy link
Member

Oh, test_gmtime() fails on 32-bit systems: I wrote #143861 to fix the test.

@bedevere-bot
Copy link

⚠️⚠️⚠️ Buildbot failure ⚠️⚠️⚠️

Hi! The buildbot ARM64 macOS 3.x (tier-2) has failed when building commit f5685a2.

What do you need to do:

  1. Don't panic.
  2. Check the buildbot page in the devguide if you don't know what the buildbots are or how they work.
  3. Go to the page of the buildbot that failed (https://buildbot.python.org/#/builders/725/builds/12561) and take a look at the build logs.
  4. Check if the failure is related to this commit (f5685a2) or if it is a false positive.
  5. If the failure is related to this commit, please, reflect that on the issue and make a new Pull Request with a fix.

You can take a look at the buildbot page here:

https://buildbot.python.org/#/builders/725/builds/12561

Failed tests:

  • test_urllib2net

Summary of the results of the build (if available):

==

Click to see traceback logs
remote: Enumerating objects: 15, done.        
remote: Counting objects:   7% (1/13)        
remote: Counting objects:  15% (2/13)        
remote: Counting objects:  23% (3/13)        
remote: Counting objects:  30% (4/13)        
remote: Counting objects:  38% (5/13)        
remote: Counting objects:  46% (6/13)        
remote: Counting objects:  53% (7/13)        
remote: Counting objects:  61% (8/13)        
remote: Counting objects:  69% (9/13)        
remote: Counting objects:  76% (10/13)        
remote: Counting objects:  84% (11/13)        
remote: Counting objects:  92% (12/13)        
remote: Counting objects: 100% (13/13)        
remote: Counting objects: 100% (13/13), done.        
remote: Compressing objects:   9% (1/11)        
remote: Compressing objects:  18% (2/11)        
remote: Compressing objects:  27% (3/11)        
remote: Compressing objects:  36% (4/11)        
remote: Compressing objects:  45% (5/11)        
remote: Compressing objects:  54% (6/11)        
remote: Compressing objects:  63% (7/11)        
remote: Compressing objects:  72% (8/11)        
remote: Compressing objects:  81% (9/11)        
remote: Compressing objects:  90% (10/11)        
remote: Compressing objects: 100% (11/11)        
remote: Compressing objects: 100% (11/11), done.        
remote: Total 15 (delta 2), reused 2 (delta 2), pack-reused 2 (from 2)        
From https://github.com/python/cpython
 * branch                    main       -> FETCH_HEAD
Note: switching to 'f5685a266b252455e03ef8e6055eaf4007ec749d'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:

  git switch -c <new-branch-name>

Or undo this operation with:

  git switch -

Turn off this advice by setting config variable advice.detachedHead to false

HEAD is now at f5685a266b2 gh-80620: Support negative timestamps on windows in `time.gmtime`, `time.localtime`, and `datetime` module (#143463)
Switched to and reset branch 'main'

make: *** [buildbottest] Error 2

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants